-
-
Notifications
You must be signed in to change notification settings - Fork 231
feat(identity): sync address book contacts #5776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
## Explanation - Emit events on contact updates and deletions. - List method to retrieve contacts stored locally. ## References Needed by: #5776 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Mathieu Artu <[email protected]>
@metamaskbot publish-preview |
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions.
|
@metamaskbot publish-preview |
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions.
|
@metamaskbot publish-preview |
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions.
|
...le-sync-controller/src/controllers/user-storage/contact-syncing/__fixtures__/mockContacts.ts
Outdated
Show resolved
Hide resolved
...file-sync-controller/src/controllers/user-storage/contact-syncing/__fixtures__/test-utils.ts
Show resolved
Hide resolved
...file-sync-controller/src/controllers/user-storage/contact-syncing/__fixtures__/test-utils.ts
Outdated
Show resolved
Hide resolved
.../profile-sync-controller/src/controllers/user-storage/contact-syncing/setup-subscriptions.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/controllers/user-storage/contact-syncing/utils.ts
Outdated
Show resolved
Hide resolved
...ofile-sync-controller/src/controllers/user-storage/contact-syncing/controller-integration.ts
Outdated
Show resolved
Hide resolved
...ofile-sync-controller/src/controllers/user-storage/contact-syncing/controller-integration.ts
Outdated
Show resolved
Hide resolved
if (onContactSyncErroneousSituation) { | ||
onContactSyncErroneousSituation('Error synchronizing contacts', { | ||
error, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should still throw the actual error here, as to me this is a real error and not an erroneous situation. FYI, we introduced the concept of erroneous situations for account syncing because we detected weird behaviors that didn't align with our other metrics, and wanted to understand corner cases.
For contacts, we might not want to have the concept of "erroneous situations" at all, at least for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might encounter erroneous situations with contact syncing as well, imho. 🤔 Especially bc of deletions.
I'd prefer to leave the callback in place, and just re-throw the error, for now.
...ofile-sync-controller/src/controllers/user-storage/contact-syncing/controller-integration.ts
Outdated
Show resolved
Hide resolved
...ofile-sync-controller/src/controllers/user-storage/contact-syncing/controller-integration.ts
Outdated
Show resolved
Hide resolved
@metamaskbot publish-preview |
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work!!
I left a suggestion and a bunch of questions. Please take a look.
*/ | ||
export type SyncAddressBookEntry = AddressBookEntry & { | ||
lastUpdatedAt?: number; | ||
deleted?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd argue that deleted
can be removed and we can use deletedAt
for both.
We can also argue the reverse: Since we have a lastUpdatedAt
property, we can combine that with the deleted
flag to compute deletedAt
.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but I prefer the 1st among your options:
- I'll get rid of
deleted
(bool) deletedAt
absent or zero means ofc "not deleted"lastUpdatedAt
keeps being only about updates (and it's persisted in AddressBook unlike deletedAt)
true, | ||
); | ||
|
||
// Get all local contacts from AddressBookController (exclude chain "*" contacts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why exclude chainId=='*'?
How sure are we this is the only situation with universal chainId?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure. If you look at the current implementation of AddressBookController
there's never a chance a contact is stored with an unknown/wildcard chain id. Perhaps in the past that was possible, and the "pet name bridge" still uses that for "account contacts" which is out of this PR's scope.
getMessenger().call( | ||
'AddressBookController:set', | ||
contact.address, | ||
contact.name || '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think contacts with empty names should be ignored.
} | ||
} | ||
|
||
// SCENARIO 3 & 5: Process local contacts not in remote |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO this also covers scenario 1. We should be able to shorten some of the implementation.
// Update existing remote contacts with new contacts | ||
const updatedRemoteContacts = [...remoteContacts]; | ||
|
||
for (const localContact of contactsToUpdateRemotely) { | ||
const key = createContactKey(localContact); | ||
const existingIndex = updatedRemoteContacts.findIndex( | ||
(c) => createContactKey(c) === key, | ||
); | ||
|
||
const now = Date.now(); | ||
const updatedEntry = { | ||
...localContact, | ||
lastUpdatedAt: now, | ||
deleted: false, // Ensure it's not deleted | ||
} as SyncAddressBookEntry; | ||
|
||
if (existingIndex !== -1) { | ||
// Update existing contact | ||
updatedRemoteContacts[existingIndex] = updatedEntry; | ||
} else { | ||
// Add new contact | ||
updatedRemoteContacts.push(updatedEntry); | ||
} | ||
} | ||
|
||
// Save updated contacts to remote storage | ||
await saveContactsToUserStorage(updatedRemoteContacts, options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not send the remote contacts back if they haven't changed locally.
Starting with const updatedRemoteContacts = [...remoteContacts];
means we're always sending the entire list of contacts.
We can index by contactKey and create a list containing only the modified or new contacts like so:
// Update existing remote contacts with new contacts | |
const updatedRemoteContacts = [...remoteContacts]; | |
for (const localContact of contactsToUpdateRemotely) { | |
const key = createContactKey(localContact); | |
const existingIndex = updatedRemoteContacts.findIndex( | |
(c) => createContactKey(c) === key, | |
); | |
const now = Date.now(); | |
const updatedEntry = { | |
...localContact, | |
lastUpdatedAt: now, | |
deleted: false, // Ensure it's not deleted | |
} as SyncAddressBookEntry; | |
if (existingIndex !== -1) { | |
// Update existing contact | |
updatedRemoteContacts[existingIndex] = updatedEntry; | |
} else { | |
// Add new contact | |
updatedRemoteContacts.push(updatedEntry); | |
} | |
} | |
// Save updated contacts to remote storage | |
await saveContactsToUserStorage(updatedRemoteContacts, options); | |
const updatedRemoteContacts: Record<string, SyncAddressBookEntry> = {}; | |
for (const localContact of contactsToUpdateRemotely) { | |
const key = createContactKey(localContact); | |
updatedRemoteContacts[key] = { | |
...remoteContactsMap.get(key), // Start with an existing remote contact if it exists | |
...localContact, // override with local changes | |
lastUpdatedAt: Date.now(), // mark as updated | |
deleted: false, // Ensure it's not deleted | |
}; | |
} | |
// Save updated contacts to remote storage | |
await saveContactsToUserStorage( | |
Object.values(updatedRemoteContacts), | |
options, | |
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. It's no longer necessary to overwrite all contacts per user/chain. I should update/delete only those who actually changed state.
|
||
return { | ||
[USER_STORAGE_VERSION_KEY]: USER_STORAGE_VERSION, | ||
a: toChecksumHexAddress(address), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd remove the checksum address here since we are going to need non-EVM contacts too
a: toChecksumHexAddress(address), | |
a: address, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, that makes sense. Although a validation on hex addresses still happens in AddressBookController atm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, but I think it's best to keep that logic there and deal with the sync assuming everything else was already validated.
userStorageEntry: UserStorageContactEntry, | ||
): SyncAddressBookEntry => { | ||
const addressBookEntry: SyncAddressBookEntry = { | ||
address: toChecksumHexAddress(userStorageEntry.a), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same argument here about non-EVM.
We can let other parts of the code deal with the address validation.
address: toChecksumHexAddress(userStorageEntry.a), | |
address: userStorageEntry.a, |
name: userStorageEntry.n, | ||
chainId: userStorageEntry.c, | ||
memo: userStorageEntry.m || '', | ||
isEns: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens to ENS contacts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooops. I forgot to map it! 🤦 Thanks! Great catch.
Explanation
The current implementation of MetaMask's address book (contacts) lacks the ability to synchronize between devices. While we recently implemented this capability for accounts using encrypted user storage, the address book remained local to each device.
This PR implements backup-and-sync functionality for the address book controller, similar to what was already implemented for the accounts controller.
Implementation notes
References
Needed by: MetaMask/metamask-extension#32632
Depends on: #5779
Checklist